﻿using System;

using QiHe.Yaml.Grammar;

namespace TracedReality
{
    /// <summary>
    /// A 3-space vector class.
    /// </summary>
    public class Vector
    {
        /// <summary>
        /// Value in the specified dimension.
        /// </summary>
        public double x, y, z;

        /// <summary>
        /// The length of this vector.
        /// </summary>
        public double length
        {
            get { return Math.Sqrt((x * x) + (y * y) + (z * z)); }
        }

        /// <summary>
        /// Create a Vector with the given values.
        /// </summary>
        /// <param name="a">Value in the x dimension.</param>
        /// <param name="b">Value in the y dimension.</param>
        /// <param name="c">Value in the z dimension.</param>
        public Vector(double a, double b, double c) { x = a; y = b; z = c; }

        /// <summary>
        /// Create a Vector with the given value.
        /// </summary>
        /// <param name="a">Value in every dimension.</param>
        public Vector(double a) : this(a, a, a) { }

        /// <summary>
        /// Create a Vector that is a copy of the given Vector.
        /// </summary>
        /// <param name="o">Vector to copy.</param>
        public Vector(Vector o) : this(o.x, o.y, o.z) { }

        /// <summary>
        /// Create a 0 length vector.
        /// </summary>
        public Vector() : this(0) { }

        /// <summary>
        /// Scale this Vector by the given amount in each dimension.
        /// </summary>
        /// <param name="a">Value to scale the x dimension with.</param>
        /// <param name="b">Value to scale the y dimension with.</param>
        /// <param name="c">Value to scale the z dimension with.</param>
        public void scale(double a, double b, double c)
        {
            x *= a;
            y *= b;
            z *= c;
        }

        /// <summary>
        /// Scale this Vector by the given amount in every dimension.
        /// </summary>
        /// <param name="a">Value to scale every dimension with.</param>
        public void scale(double a) { scale(a, a, a); }
        
        /// <summary>
        /// Normalize this Vector to have a length of 1.
        /// </summary>
        public void normalize() { scale(1 / length); }

        /// <summary>
        /// Get the average value of the three dimensions.
        /// </summary>
        /// <returns>The average of the dimensions.</returns>
        public double average() { return (x + y + z) / 3.0; }

        /// <summary>
        /// Get the max value of the three dimensions.
        /// </summary>
        /// <returns>The maximum of the dimensions.</returns>
        public double max() { return Math.Max(x, Math.Max(y, z)); }

        /// <summary>
        /// Get the min value of the three dimensions.
        /// </summary>
        /// <returns>The minimum of the dimensions.</returns>
        public double min() { return Math.Min(x, Math.Min(y, z)); }

        /// <summary>
        /// Compute the dot product of this Vector with another Vector.
        /// </summary>
        /// <param name="o">The other Vector.</param>
        /// <returns>The dot product of the two Vectors.</returns>
        public double dot(Vector o) { return (x * o.x) + (y * o.y) + (z * o.z); }

        /// <summary>
        /// Compute the cross product of this Vector with another Vector.
        /// </summary>
        /// <param name="o">The other Vector.</param>
        /// <returns>The cross product of the two Vectors.</returns>
        public Vector cross(Vector o)
        {
            double a = (y * o.z) - (z * o.y);
            double b = (z * o.x) - (x * o.z);
            double c = (x * o.y) - (y * o.x);

            return new Vector(a, b, c);
        }

        public double innerProduct(Vector o)
        {
            return x * o.x + y * o.y + z * o.z;
        }

        public double distance(Vector o)
        {
            double dx = x - o.x;
            double dy = y - o.y;
            double dz = z - o.z;

            return Math.Sqrt((dx * dx) + (dy * dy) + (dz * dz));
        }

        public override bool Equals(object o)
        {
            if (o is Vector) { return Equals((Vector)o); }
            return false;
        }

        public bool Equals(Vector v)
        {
            return (x == v.x && y == v.y && z == v.z);
        }

        public override int GetHashCode()
        {
            return ToString().GetHashCode();
        }

        #region operators
        public static Vector operator -(Vector a)
        {
            return new Vector(-a.x, -a.y, -a.z);
        }

        public static Vector operator +(Vector a, Vector b)
        {
            return new Vector(a.x + b.x, a.y + b.y, a.z + b.z);
        }

        public static Vector operator +(Vector a, double d)
        {
            return new Vector(a.x + d, a.y + d, a.z + d);
        }
        
        public static Vector operator -(Vector a, Vector b)
        {
            return new Vector(a.x - b.x, a.y - b.y, a.z - b.z);
        }

        public static Vector operator -(Vector a, double d)
        {
            return new Vector(a.x - d, a.y - d, a.z - d);
        }

        public static Vector operator *(Vector a, Vector b)
        {
            return new Vector(a.x * b.x, a.y * b.y, a.z * b.z);
        }

        public static Vector operator *(Vector a, double d)
        {
            return new Vector(a.x * d, a.y * d, a.z * d);
        }

        public static Vector operator *(double d, Vector a)
        {
            return new Vector(a.x * d, a.y * d, a.z * d);
        }

        public static Vector operator /(Vector a, double d)
        {
            return new Vector(a.x / d, a.y / d, a.z / d);
        }

        public static Vector operator /(double d, Vector a)
        {
            return new Vector(a.x / d, a.y / d, a.z / d);
        }

        public static explicit operator System.Drawing.Color(Vector v)
        {
            Vector normalized = new Vector();
            normalized.x = Utilities.linearToSRGB(v.x);
            normalized.y = Utilities.linearToSRGB(v.y);
            normalized.z = Utilities.linearToSRGB(v.z);

            normalized = Vector.Max(0, Vector.Min(1, normalized));

            normalized *= 255;

            return System.Drawing.Color.FromArgb((int)normalized.x, (int)normalized.y, (int)normalized.z);
        }

        public static explicit operator float[](Vector v)
        {
            return new float[] { (float)v.x, (float)v.y, (float)v.z };
        }

        public static explicit operator double[](Vector v)
        {
            return new double[] { v.x, v.y, v.z };
        }
        #endregion

        /// <summary>
        /// Gets a string representing this Vector.
        /// </summary>
        /// <returns>A string representation of this Vector.</returns>
        public override string ToString()
        {
            return "[" + x + ", " + y + ", " + z + "]";
        }

        #region Min/Max
        public static Vector Min(Vector a, double b)
        {
            if (a == null) { return null; }

            return new Vector(Math.Min(a.x, b), Math.Min(a.y, b), Math.Min(a.z, b));
        }

        public static Vector Min(double b, Vector a)
        {
            if (a == null) { return null; }

            return new Vector(Math.Min(a.x, b), Math.Min(a.y, b), Math.Min(a.z, b));
        }

        public static Vector Min(Vector a, Vector b)
        {
            if (a == null && b == null) { return null; }
            else if (a == null) { return b; }
            else if (b == null) { return a; }

            return new Vector(Math.Min(a.x, b.x), Math.Min(a.y, b.y), Math.Min(a.z, b.z));
        }

        public static Vector Max(Vector a, double b)
        {
            if (a == null) { return null; }

            return new Vector(Math.Max(a.x, b), Math.Max(a.y, b), Math.Max(a.z, b));
        }

        public static Vector Max(double b, Vector a)
        {
            if (a == null) { return null; }

            return new Vector(Math.Max(a.x, b), Math.Max(a.y, b), Math.Max(a.z, b));
        }

        public static Vector Max(Vector a, Vector b)
        {
            if (a == null && b == null) { return null; }
            else if (a == null) { return b; }
            else if (b == null) { return a; }

            return new Vector(Math.Max(a.x, b.x), Math.Max(a.y, b.y), Math.Max(a.z, b.z));
        }
        #endregion

        /// <summary>
        /// Compute the euclidean distance between two Vectors.
        /// </summary>
        /// <param name="a">The first Vector.</param>
        /// <param name="b">The second Vector.</param>
        /// <returns>The distance between the given Vectors.</returns>
        public static double Distance(Vector a, Vector b)
        {
            double dx = a.x - b.x;
            double dy = a.y - b.y;
            double dz = a.z - b.z;

            return Math.Sqrt((dx * dx) + (dy * dy) + (dz * dz));
        }

        /// <summary>
        /// Converts a Color to a Vector after a conversion from sRGB to linear.
        /// </summary>
        /// <param name="c">The Color to convert</param>
        /// <returns>A Vector representation of the given Color.</returns>
        public static Vector parseColor(System.Drawing.Color c)
        {
            Vector normalized = new Vector();
            normalized.x = Utilities.srgbToLinear(c.R);
            normalized.y = Utilities.srgbToLinear(c.G);
            normalized.z = Utilities.srgbToLinear(c.B);

            normalized = Vector.Max(0, Vector.Min(1, normalized));

            return normalized;
        }

        public static Vector FromSequence(Sequence seq)
        {
            if (seq == null) { throw new ArgumentNullException("seq"); }
            if (seq.Entries.Count != 3) { throw new ArgumentException("Sequence must have 3 values", "seq"); }

            double a = double.Parse(seq.Entries[0].ToString());
            double b = double.Parse(seq.Entries[1].ToString());
            double c = double.Parse(seq.Entries[2].ToString());

            return new Vector(a, b, c);
        }

        /// <summary>
        /// Create a random unit Vector.
        /// </summary>
        /// <param name="r">Random to use.</param>
        /// <returns>A random unit Vector.</returns>
        public static Vector RandomUnitVector(Random r)
        {
            double x = 0;
            double y = 0;
            double z = 0;

            do
            {
                x = (2 * r.NextDouble()) - 1;
                y = (2 * r.NextDouble()) - 1;
                z = (2 * r.NextDouble()) - 1;
            } while (((x * x) + (y * y) + (z * z)) > 1);

            Vector v = new Vector(x, y, z);
            v.normalize();

            return v;
        }

        /// <summary>
        /// Create a random unit Vector in the hemisphere around a normal.
        /// </summary>
        /// <param name="n">The normal to place the hemisphere around.</param>
        /// <param name="r">Random to use.</param>
        /// <returns>A random diffuse Vector around the normal.</returns>
        public static Vector RandomDiffuseVector(Vector n, Random r)
        {
            Vector v = n + RandomUnitVector(r);
            v.normalize();

            return v;
        }

        /// <summary>
        /// Compute the reflected Vector of the given incoming Vector and normal.
        /// </summary>
        /// <param name="inc">The incoming Vector.</param>
        /// <param name="n">The normal.</param>
        /// <returns>The reflection of the given Vector around the normal.</returns>
        public static Vector Reflection(Vector inc, Vector n)
        {
            double dot = inc.dot(n);
            Vector v = inc - (n * 2 * dot);
            v.normalize();

            return v;
        }

        /// <summary>
        /// Compute the refracted Vector of the given incoming Vector and normal with
        /// the given indices of refraction.
        /// </summary>
        /// <param name="inc">The incoming Vector.</param>
        /// <param name="n">The normal.</param>
        /// <param name="eta1">The index of refraction of the "outside" material.</param>
        /// <param name="eta2">The index of refraction of the "inside" material</param>
        /// <returns>The refraction of the given Vector around the normal with the given indices of refraction.
        /// If total internal reflection should occur, this returns a 0 length Vector.</returns>
        public static Vector Refraction(Vector inc, Vector n, double eta1, double eta2)
        {
            double eta = eta1 / eta2;
            double cosi = -inc.dot(n);
            double sint = (eta * eta) * (1 - (cosi * cosi));
            if (sint > 1) { return new Vector(); }
            double cost = Math.Sqrt(1 - sint);
            Vector sDir = (eta * inc) + ((eta * cosi) - cost) * n;
            sDir.normalize();
            return sDir;
        }
    }
}
